Skip to content

Conversation

@cj-vana
Copy link
Collaborator

@cj-vana cj-vana commented Sep 30, 2025

User description

Implements a comprehensive technical rider management system for touring artists and production teams.

Features:

  • Complete rider editor with artist info, band members, and contacts
  • Input/channel list management with mic specs and requirements
  • Sound system requirements (PA, monitors, console)
  • Backline and artist-provided equipment tracking
  • Technical staff requirements
  • Special requirements, power, lighting, and hospitality sections
  • Share functionality with view/edit permissions and expiration
  • Professional print-friendly PDF exports with proper page breaks
  • Integration with Production page
  • Database migrations for technical_riders table and RLS policies
  • Shared view and edit pages with routing
  • Updated share utilities to support technical_rider resource type

Technical:

  • Created RiderEditor, AllRiders, SharedTechnicalRider components
  • Added RiderExport component for display and PDF generation
  • Database: technical_riders table with JSONB columns for arrays
  • RLS policies for owner access and shared link permissions
  • jsPDF + autoTable for print exports (matching production schedules)
  • Updated version to 1.5.6.8

PR Type

Enhancement, Documentation


Description

• Implements comprehensive technical rider management system for touring artists and production teams
• Creates complete rider editor with tabbed interface for artist info, band members, input channels, equipment, and staff requirements
• Adds professional PDF export capabilities with both color and print-friendly versions using jsPDF
• Implements sharing functionality with view/edit permissions and expiration controls
• Integrates technical riders into production documents dashboard
• Creates database schema with technical_riders table using JSONB columns for complex data structures
• Establishes RLS policies for secure access control and shared link permissions
• Adds comprehensive TypeScript interfaces for all technical rider data structures
• Updates application routing to support rider management and shared access pages
• Version bump to 1.5.6.8 with complete changelog documentation


Diagram Walkthrough

flowchart LR
  A["RiderEditor"] --> B["Database"]
  B --> C["AllRiders"]
  C --> D["PDF Export"]
  C --> E["Share Links"]
  E --> F["SharedTechnicalRider"]
  G["ProductionPage"] --> C
  B --> H["RLS Policies"]
Loading

File Walkthrough

Relevant files
Enhancement
12 files
shareUtils.ts
Extended share utilities to support technical rider resources

apps/web/src/lib/shareUtils.ts

• Added technical_rider to the ResourceType union type
• Added case
handling for technical_rider in getSharedResource function to map to
technical_riders table
• Added URL generation logic for technical
rider share links with view/edit permissions

+13/-3   
types.ts
Added TypeScript interfaces for technical rider data structures

apps/web/src/lib/types.ts

• Added comprehensive TypeScript interfaces for technical rider data
structures
• Defined BandMember, InputChannel, BacklineItem,
StaffRequirement interfaces
• Created RiderForExport interface
containing all technical rider fields including JSONB arrays

+59/-0   
AllRiders.tsx
Complete technical rider management page with export and sharing

apps/web/src/pages/AllRiders.tsx

• Created comprehensive technical rider management page with CRUD
operations
• Implemented search, sorting, and filtering functionality
for rider list
• Added PDF export capabilities with both color and
print-friendly versions using jsPDF
• Integrated share functionality
and delete confirmation modals

+1018/-0
ProductionPage.tsx
Integrated technical riders into production documents dashboard

apps/web/src/pages/ProductionPage.tsx

• Added technical riders section to production documents dashboard

Integrated rider fetching, display, and management functionality

Added export capabilities for technical riders with PDF generation

Extended delete confirmation to handle rider document type

+620/-6 
RiderExport.tsx
Professional color export component for technical riders 

apps/web/src/components/rider/RiderExport.tsx

• Created color export component for technical riders with
professional styling
• Implemented comprehensive layout with sections
for contact info, band members, input list, equipment, and
requirements
• Added gradient backgrounds and professional branding
elements
• Designed for PDF export with proper formatting and
responsive design

+356/-0 
PrintRiderExport.tsx
Print-optimized export component for technical riders       

apps/web/src/components/rider/PrintRiderExport.tsx

• Created print-friendly export component optimized for black and
white printing
• Implemented clean table layouts and proper typography
for professional documents
• Added page break considerations and
print-specific styling
• Designed for high-quality PDF generation with
minimal ink usage

+402/-0 
RiderEquipment.tsx
Equipment management component for technical rider editor

apps/web/src/components/rider/RiderEquipment.tsx

• Created equipment management component for technical riders

Implemented dynamic forms for PA, monitor, and console requirements

Added CRUD operations for backline requirements and artist-provided
gear
• Included validation and user-friendly interface elements

+298/-0 
RiderEditor.tsx
Technical Rider Editor with Tabbed Interface and Database Integration

apps/web/src/pages/RiderEditor.tsx

• Creates a comprehensive technical rider editor with tabbed interface
for artist info, input list, equipment, and staff sections

Implements full CRUD operations with Supabase integration for
creating, reading, updating, and deleting technical riders
• Provides
state management for rider data with real-time saving functionality
and error handling
• Includes navigation controls and responsive
design with mobile-friendly layout

+379/-0 
RiderInputList.tsx
Input Channel List Component with Responsive Design           

apps/web/src/components/rider/RiderInputList.tsx

• Implements input/channel list management with add, delete, and
duplicate functionality
• Provides responsive table view for desktop
and card view for mobile devices
• Includes form controls for channel
details like mic type, phantom power, and DI requirements
• Uses
InputChannel type for structured data management with UUID-based
identification

+298/-0 
RiderTechnicalStaff.tsx
Technical Staff and Special Requirements Management Component

apps/web/src/components/rider/RiderTechnicalStaff.tsx

• Creates staff requirements management with add/delete functionality
for technical positions
• Implements multiple text areas for special
requirements, power, lighting, and hospitality notes
• Provides
structured form inputs for staff roles, quantities, and specific
requirements
• Uses StaffRequirement type for consistent data
structure

+240/-0 
RiderArtistInfo.tsx
Artist Information and Band Member Management Component   

apps/web/src/components/rider/RiderArtistInfo.tsx

• Implements artist information form with contact details and genre
fields
• Provides band member management with add/delete functionality
and instrument tracking
• Includes input requirements specification
for each band member
• Uses BandMember type for structured member data
with UUID identification

+239/-0 
SharedTechnicalRider.tsx
Shared Technical Rider View Page with Export Integration 

apps/web/src/pages/SharedTechnicalRider.tsx

• Creates shared technical rider view page with read-only access via
share codes
• Implements error handling for invalid or expired share
links
• Integrates with RiderExport component for professional display
formatting
• Provides loading states and navigation controls for
shared content

+141/-0 
Miscellaneous
1 files
index.html
Version bump to 1.5.6.8                                                                   

apps/web/index.html

• Updated software version from "1.5.6.7" to "1.5.6.8" in structured
data

+1/-1     
Configuration changes
4 files
20250930110000_add_technical_riders_to_shared_links.sql
Database Migration for Technical Rider Sharing Support     

supabase/migrations/20250930110000_add_technical_riders_to_shared_links.sql

• Updates shared links constraint to include technical_rider as valid
resource type
• Implements comprehensive RLS policies for technical
riders with owner and shared access controls
• Creates public access
policy for view-only sharing via share codes
• Establishes proper
security boundaries for technical rider sharing functionality

+140/-0 
20250930100000_create_technical_riders.sql
Technical Riders Database Table Creation and Schema           

supabase/migrations/20250930100000_create_technical_riders.sql

• Creates technical_riders table with comprehensive schema for all
rider data
• Uses JSONB columns for arrays like band members, input
list, and equipment requirements
• Implements RLS policies for
user-owned data access and modification
• Adds automatic timestamp
triggers for tracking creation and modification dates

+78/-0   
App.tsx
Application Routing for Technical Rider Management             

apps/web/src/App.tsx

• Adds routing for technical rider editor, all riders list, and shared
rider pages
• Implements protected routes for authenticated rider
management
• Includes shared technical rider routes for both view and
edit access
• Integrates new rider components into application routing
structure

+22/-0   
package.json
Package Version Update to 1.5.6.8                                               

apps/web/package.json

• Updates package version from 1.5.6.7 to 1.5.6.8
• Reflects new
release version for technical rider feature addition

+1/-1     
Documentation
1 files
CHANGELOG.md
Version 1.5.6.8 Release Documentation                                       

CHANGELOG.md

• Documents version 1.5.6.8 release with comprehensive technical rider
system
• Details all new features including sharing, PDF export, and
database integration
• Includes script import instructions and trusted
by section additions
• Updates version information and feature
descriptions

+28/-0   
Formatting
1 files
VideoPage.tsx
Video Page Grid Spacing Adjustment                                             

apps/web/src/pages/VideoPage.tsx

• Updates grid gap spacing from 6 to 8 for better visual separation

Minor styling adjustment for improved layout consistency

+1/-1     

Implements a comprehensive technical rider management system for touring artists and production teams.

  Features:
  - Complete rider editor with artist info, band members, and contacts
  - Input/channel list management with mic specs and requirements
  - Sound system requirements (PA, monitors, console)
  - Backline and artist-provided equipment tracking
  - Technical staff requirements
  - Special requirements, power, lighting, and hospitality sections
  - Share functionality with view/edit permissions and expiration
  - Professional print-friendly PDF exports with proper page breaks
  - Integration with Production page
  - Database migrations for technical_riders table and RLS policies
  - Shared view and edit pages with routing
  - Updated share utilities to support technical_rider resource type

  Technical:
  - Created RiderEditor, AllRiders, SharedTechnicalRider components
  - Added RiderExport component for display and PDF generation
  - Database: technical_riders table with JSONB columns for arrays
  - RLS policies for owner access and shared link permissions
  - jsPDF + autoTable for print exports (matching production schedules)
  - Updated version to 1.5.6.8
@netlify
Copy link

netlify bot commented Sep 30, 2025

Deploy Preview for sounddocsbeta ready!

Name Link
🔨 Latest commit df68ca6
🔍 Latest deploy log https://app.netlify.com/projects/sounddocsbeta/deploys/68dc32619543bc00083b9556
😎 Deploy Preview https://deploy-preview-105--sounddocsbeta.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@qodo-code-review
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 Security concerns

Data exposure via RLS:
The "Public can select technical riders via valid view share code" policy allows selection if a valid shared_links row exists, without correlating to the provided share code. While comments mention RPC validation, any direct table queries that don’t use the share-code-validated path could expose data if a valid link exists. Ensure all unauthenticated or public access paths fetch through RPC that verifies share_code, and consider tightening the policy to require a verified context (e.g., using a security definer RPC) rather than broad public SELECT.

⚡ Recommended focus areas for review

PDF Export Robustness

Direct jsPDF autoTable usage relies on augmentation of the jsPDF instance; if autoTable plugin isn’t globally attached, runtime errors will occur. Also, multiple large autoTables and page-break conditions depend on magic numbers (e.g., 600/650) that may clip content on different page sizes. Validate plugin availability and verify page overflow handling with varied data sizes.

  }
};

const prepareAndExecuteRiderExport = async (riderId: string, type: "color" | "print") => {
  setExportingItemId(riderId);
  setShowRiderExportModal(false);
  try {
    const { data, error } = await supabase
      .from("technical_riders")
      .select("*")
      .eq("id", riderId)
      .single();
    if (error || !data) throw error || new Error("Technical Rider not found");

    const riderData = {
      ...data,
      band_members: data.band_members || [],
      input_list: data.input_list || [],
      backline_requirements: data.backline_requirements || [],
      artist_provided_gear: data.artist_provided_gear || [],
      required_staff: data.required_staff || [],
    } as RiderForExport;

    if (type === "color") {
      setCurrentExportRider(riderData);
      await new Promise((resolve) => setTimeout(resolve, 50));
      await exportAsPdf(
        riderExportRef,
        riderData.name,
        "technical-rider-color",
        "#111827",
        "Inter",
      );
    } else {
      // Use jsPDF directly with autoTable for print-friendly version
      try {
        const pdf = new jsPDF("p", "pt", "letter");

        const addPageHeader = (doc: jsPDF, title: string) => {
          doc.setFontSize(24);
          doc.setFont("helvetica", "bold");
          doc.text("SoundDocs", 40, 50);
          doc.setFontSize(12);
          doc.setFont("helvetica", "normal");
          doc.setTextColor(100, 100, 100);
          doc.text("Technical Rider", 40, 68);
          doc.setFontSize(16);
          doc.setFont("helvetica", "bold");
          doc.setTextColor(0, 0, 0);
          doc.text(title, 40, 90);
          doc.setDrawColor(221, 221, 221);
          doc.line(40, 100, doc.internal.pageSize.width - 40, 100);
        };

        const addPageFooter = (doc: jsPDF) => {
          const pageCount = doc.getNumberOfPages();
          const pageWidth = doc.internal.pageSize.getWidth();
          const pageHeight = doc.internal.pageSize.getHeight();

          for (let i = 1; i <= pageCount; i++) {
            doc.setPage(i);
            doc.setDrawColor(221, 221, 221);
            doc.line(40, pageHeight - 35, pageWidth - 40, pageHeight - 35);
            doc.setFontSize(8);
            doc.setTextColor(128, 128, 128);
            doc.setFont("helvetica", "bold");
            doc.text("SoundDocs", 40, pageHeight - 20);
            doc.setFont("helvetica", "normal");
            doc.text("| Professional Event Documentation", 95, pageHeight - 20);
            const pageNumText = `Page ${i} of ${pageCount}`;
            doc.text(pageNumText, pageWidth / 2, pageHeight - 20, { align: "center" });
            const dateStr = `Generated on: ${new Date().toLocaleDateString()}`;
            doc.text(dateStr, pageWidth - 40, pageHeight - 20, { align: "right" });
          }
        };

        addPageHeader(pdf, riderData.artist_name || "Artist Name");

        let lastY = 120;

        // Contact Information
        const contactInfo: [string, string][] = [
          ["Contact Name:", riderData.contact_name || "N/A"],
          ["Email:", riderData.contact_email || "N/A"],
          ["Phone:", riderData.contact_phone || "N/A"],
          ["Genre:", riderData.genre || "N/A"],
        ];

        pdf.setFontSize(12);
        pdf.setFont("helvetica", "bold");
        pdf.text("Primary Contact", 40, lastY);

        (
          pdf as unknown as {
            autoTable: (opts: unknown) => void;
            lastAutoTable: { finalY: number };
          }
        ).autoTable({
          body: contactInfo,
          startY: lastY + 10,
          theme: "plain",
          styles: {
            font: "helvetica",
            fontSize: 10,
            cellPadding: { top: 3, right: 5, bottom: 3, left: 0 },
          },
          columnStyles: {
            0: { fontStyle: "bold", cellWidth: 100 },
          },
          margin: { left: 40 },
        });
        lastY =
          (pdf as unknown as { lastAutoTable: { finalY: number } }).lastAutoTable.finalY + 20;

        // Band Members
        if (riderData.band_members && riderData.band_members.length > 0) {
          pdf.setFontSize(12);
          pdf.setFont("helvetica", "bold");
          pdf.text("Band Members", 40, lastY);

          (
            pdf as unknown as {
              autoTable: (opts: unknown) => void;
              lastAutoTable: { finalY: number };
            }
          ).autoTable({
            head: [["Name", "Instrument", "Input Needs"]],
            body: riderData.band_members.map((m) => [
              m.name || "-",
              m.instrument || "-",
              m.input_needs || "-",
            ]),
            startY: lastY + 10,
            theme: "grid",
            headStyles: { fillColor: [30, 30, 30], textColor: 255, fontStyle: "bold" },
            styles: {
              font: "helvetica",
              fontSize: 9,
              cellPadding: 5,
            },
            margin: { left: 40, right: 40 },
          });
          lastY =
            (pdf as unknown as { lastAutoTable: { finalY: number } }).lastAutoTable.finalY + 20;
        }

        // Input List
        if (riderData.input_list && riderData.input_list.length > 0) {
          // Check if we need a new page
          if (lastY > 600) {
            pdf.addPage();
            lastY = 60;
          }

          pdf.setFontSize(12);
          pdf.setFont("helvetica", "bold");
          pdf.text("Input/Channel List", 40, lastY);

          (
            pdf as unknown as {
              autoTable: (opts: unknown) => void;
              lastAutoTable: { finalY: number };
            }
          ).autoTable({
            head: [["Ch", "Name", "Type", "Mic", "48V", "DI", "Notes"]],
            body: riderData.input_list.map((input) => [
              input.channel_number || "-",
              input.name || "-",
              input.type || "-",
              input.mic_type || "-",
              input.phantom_power ? "✓" : "",
              input.di_needed ? "✓" : "",
              input.notes || "",
            ]),
            startY: lastY + 10,
            theme: "grid",
            headStyles: { fillColor: [30, 30, 30], textColor: 255, fontStyle: "bold" },
            styles: {
              font: "helvetica",
              fontSize: 8,
              cellPadding: 4,
            },
            columnStyles: {
              0: { cellWidth: 30 },
              4: { halign: "center", cellWidth: 30 },
              5: { halign: "center", cellWidth: 30 },
            },
            margin: { left: 40, right: 40 },
          });
          lastY =
            (pdf as unknown as { lastAutoTable: { finalY: number } }).lastAutoTable.finalY + 20;
        }

        // Sound System Requirements
        if (lastY > 650) {
          pdf.addPage();
          lastY = 60;
        }

        pdf.setFontSize(12);
        pdf.setFont("helvetica", "bold");
        pdf.text("Sound System Requirements", 40, lastY);
        lastY += 15;

        pdf.setFontSize(10);
        pdf.setFont("helvetica", "bold");
        pdf.text("PA System:", 40, lastY);
        pdf.setFont("helvetica", "normal");
        const paLines = pdf.splitTextToSize(
          riderData.pa_requirements || "Not specified",
          pdf.internal.pageSize.width - 80,
        );
        pdf.text(paLines, 40, lastY + 15);
        lastY += 15 + paLines.length * 12 + 10;

        pdf.setFont("helvetica", "bold");
        pdf.text("Monitor System:", 40, lastY);
        pdf.setFont("helvetica", "normal");
        const monitorLines = pdf.splitTextToSize(
          riderData.monitor_requirements || "Not specified",
          pdf.internal.pageSize.width - 80,
        );
        pdf.text(monitorLines, 40, lastY + 15);
        lastY += 15 + monitorLines.length * 12 + 10;

        pdf.setFont("helvetica", "bold");
        pdf.text("Console Requirements:", 40, lastY);
        pdf.setFont("helvetica", "normal");
        const consoleLines = pdf.splitTextToSize(
          riderData.console_requirements || "Not specified",
          pdf.internal.pageSize.width - 80,
        );
        pdf.text(consoleLines, 40, lastY + 15);
        lastY += 15 + consoleLines.length * 12 + 20;

        // Backline Requirements
        if (riderData.backline_requirements && riderData.backline_requirements.length > 0) {
          if (lastY > 650) {
            pdf.addPage();
            lastY = 60;
          }

          pdf.setFontSize(12);
          pdf.setFont("helvetica", "bold");
          pdf.text("Venue Provided Backline", 40, lastY);

          (
            pdf as unknown as {
              autoTable: (opts: unknown) => void;
              lastAutoTable: { finalY: number };
            }
          ).autoTable({
            head: [["Item", "Quantity", "Specifications"]],
            body: riderData.backline_requirements.map((item) => [
              item.item || "-",
              item.quantity || "-",
              item.notes || "-",
            ]),
            startY: lastY + 10,
            theme: "grid",
            headStyles: { fillColor: [30, 30, 30], textColor: 255, fontStyle: "bold" },
            styles: {
              font: "helvetica",
              fontSize: 9,
              cellPadding: 5,
            },
            margin: { left: 40, right: 40 },
          });
          lastY =
            (pdf as unknown as { lastAutoTable: { finalY: number } }).lastAutoTable.finalY + 20;
        }

        // Artist Provided Gear
        if (riderData.artist_provided_gear && riderData.artist_provided_gear.length > 0) {
          if (lastY > 650) {
            pdf.addPage();
            lastY = 60;
          }

          pdf.setFontSize(12);
          pdf.setFont("helvetica", "bold");
          pdf.text("Artist Provided Equipment", 40, lastY);

          (
            pdf as unknown as {
              autoTable: (opts: unknown) => void;
              lastAutoTable: { finalY: number };
            }
          ).autoTable({
            head: [["Item", "Quantity", "Specifications"]],
            body: riderData.artist_provided_gear.map((item) => [
              item.item || "-",
              item.quantity || "-",
              item.notes || "-",
            ]),
            startY: lastY + 10,
            theme: "grid",
            headStyles: { fillColor: [30, 30, 30], textColor: 255, fontStyle: "bold" },
            styles: {
              font: "helvetica",
              fontSize: 9,
              cellPadding: 5,
            },
            margin: { left: 40, right: 40 },
          });
          lastY =
            (pdf as unknown as { lastAutoTable: { finalY: number } }).lastAutoTable.finalY + 20;
        }

        // Required Staff
        if (riderData.required_staff && riderData.required_staff.length > 0) {
          if (lastY > 650) {
            pdf.addPage();
            lastY = 60;
          }

          pdf.setFontSize(12);
          pdf.setFont("helvetica", "bold");
          pdf.text("Required Technical Staff", 40, lastY);

          (
            pdf as unknown as {
              autoTable: (opts: unknown) => void;
              lastAutoTable: { finalY: number };
            }
          ).autoTable({
            head: [["Role", "Quantity", "Requirements"]],
            body: riderData.required_staff.map((staff) => [
              staff.role || "-",
              staff.quantity || "-",
              staff.notes || "-",
            ]),
            startY: lastY + 10,
            theme: "grid",
            headStyles: { fillColor: [30, 30, 30], textColor: 255, fontStyle: "bold" },
            styles: {
              font: "helvetica",
              fontSize: 9,
              cellPadding: 5,
            },
            margin: { left: 40, right: 40 },
          });
          lastY =
            (pdf as unknown as { lastAutoTable: { finalY: number } }).lastAutoTable.finalY + 20;
        }

        // Special Requirements
        if (lastY > 650) {
          pdf.addPage();
          lastY = 60;
        }

        pdf.setFontSize(12);
        pdf.setFont("helvetica", "bold");
        pdf.text("Special Requirements", 40, lastY);
        lastY += 15;

        if (riderData.special_requirements) {
          pdf.setFontSize(10);
          pdf.setFont("helvetica", "bold");
          pdf.text("Stage & Production:", 40, lastY);
          pdf.setFont("helvetica", "normal");
          const specialLines = pdf.splitTextToSize(
            riderData.special_requirements,
            pdf.internal.pageSize.width - 80,
          );
          pdf.text(specialLines, 40, lastY + 15);
          lastY += 15 + specialLines.length * 12 + 10;
        }

        if (riderData.power_requirements) {
          pdf.setFont("helvetica", "bold");
          pdf.text("Power Requirements:", 40, lastY);
          pdf.setFont("helvetica", "normal");
          const powerLines = pdf.splitTextToSize(
            riderData.power_requirements,
            pdf.internal.pageSize.width - 80,
          );
          pdf.text(powerLines, 40, lastY + 15);
          lastY += 15 + powerLines.length * 12 + 10;
        }

        if (riderData.lighting_notes) {
          pdf.setFont("helvetica", "bold");
          pdf.text("Lighting:", 40, lastY);
          pdf.setFont("helvetica", "normal");
          const lightingLines = pdf.splitTextToSize(
            riderData.lighting_notes,
            pdf.internal.pageSize.width - 80,
          );
          pdf.text(lightingLines, 40, lastY + 15);
          lastY += 15 + lightingLines.length * 12 + 10;
        }

        // Hospitality
        if (riderData.hospitality_notes) {
          if (lastY > 650) {
            pdf.addPage();
            lastY = 60;
          }

          pdf.setFontSize(12);
          pdf.setFont("helvetica", "bold");
          pdf.text("Hospitality", 40, lastY);
          pdf.setFontSize(10);
          pdf.setFont("helvetica", "normal");
          const hospitalityLines = pdf.splitTextToSize(
            riderData.hospitality_notes,
            pdf.internal.pageSize.width - 80,
          );
          pdf.text(hospitalityLines, 40, lastY + 15);
          lastY += 15 + hospitalityLines.length * 12 + 10;
        }

        // Additional Notes
        if (riderData.additional_notes) {
          if (lastY > 650) {
            pdf.addPage();
            lastY = 60;
          }

          pdf.setFontSize(12);
          pdf.setFont("helvetica", "bold");
          pdf.text("Additional Notes", 40, lastY);
          pdf.setFontSize(10);
          pdf.setFont("helvetica", "normal");
          const additionalLines = pdf.splitTextToSize(
            riderData.additional_notes,
            pdf.internal.pageSize.width - 80,
          );
          pdf.text(additionalLines, 40, lastY + 15);
        }

        addPageFooter(pdf);
        pdf.save(
          `${riderData.name.replace(/\s+/g, "-").toLowerCase()}-technical-rider-print.pdf`,
        );
      } catch (error) {
        console.error("Error exporting print-friendly PDF:", error);
        setSupabaseError("Failed to export print-friendly PDF. See console for details.");
      }
    }
  } catch (error) {
    console.error("Error exporting technical rider:", error);
    setSupabaseError("Failed to export technical rider. Please try again.");
  } finally {
    setExportingItemId(null);
    setCurrentExportRider(null);
    setExportRiderId(null);
  }
};
Routing Consistency

Added resource type 'technical_rider' returns URLs under '/shared/technical-rider/...'. Ensure these precisely match the new route definitions and any share link validators; confirm no mismatch with kebab vs underscore naming across DB 'resource_type' and frontend paths.

    return `${baseUrl}/shared/theater-mic-plot/edit/${shareCode}`;
  }
  return `${baseUrl}/shared/theater-mic-plot/${shareCode}`;
} else if (resourceType === "technical_rider") {
  // Added case for technical_rider
  if (linkType === "edit") {
    return `${baseUrl}/shared/technical-rider/edit/${shareCode}`;
  }
  return `${baseUrl}/shared/technical-rider/${shareCode}`;
}
RLS Policy Semantics

Shared access policies mix claimed-share logic and a public view policy that allows selection if any valid link exists, independent of claim. Confirm this doesn’t unintentionally expose riders via other users’ valid (but undisclosed) links and that RPC share-code validation is always enforced in app queries.

CREATE POLICY "Users can select technical riders shared with them for viewing"
  ON public.technical_riders FOR SELECT
  TO authenticated
  USING (
    EXISTS (
      SELECT 1
      FROM shared_links sl
      LEFT JOIN user_claimed_shares ucs ON sl.id = ucs.shared_link_id AND ucs.user_id = auth.uid()
      WHERE sl.resource_id = technical_riders.id
        AND sl.resource_type = 'technical_rider'
        AND (sl.link_type = 'view' OR sl.link_type = 'edit')
        AND (sl.expires_at IS NULL OR sl.expires_at > now())
        AND ( -- Either claimed by the user OR it's a link that doesn't strictly require claiming for view (depends on app logic)
              ucs.id IS NOT NULL OR
              NOT EXISTS (SELECT 1 FROM user_claimed_shares ucs_check WHERE ucs_check.shared_link_id = sl.id) -- if no one claimed it, it's open if link is valid
            )
    )
  );

CREATE POLICY "Users can update technical riders shared with them for editing"
  ON public.technical_riders FOR UPDATE
  TO authenticated
  USING (
    EXISTS (
      SELECT 1
      FROM shared_links sl
      JOIN user_claimed_shares ucs ON sl.id = ucs.shared_link_id -- Must be claimed for edit
      WHERE sl.resource_id = technical_riders.id
        AND sl.resource_type = 'technical_rider'
        AND sl.link_type = 'edit'
        AND ucs.user_id = auth.uid()
        AND (sl.expires_at IS NULL OR sl.expires_at > now())
    )
  )
  WITH CHECK (
    EXISTS (
      SELECT 1
      FROM shared_links sl
      JOIN user_claimed_shares ucs ON sl.id = ucs.shared_link_id
      WHERE sl.resource_id = technical_riders.id
        AND sl.resource_type = 'technical_rider'
        AND sl.link_type = 'edit'
        AND ucs.user_id = auth.uid()
        AND (sl.expires_at IS NULL OR sl.expires_at > now())
    )
  );

-- Public view access for shared links (used by shared view pages before login/claiming)
CREATE POLICY "Public can select technical riders via valid view share code"
  ON public.technical_riders FOR SELECT
  TO public, authenticated
  USING (
    EXISTS (
      SELECT 1
      FROM public.shared_links sl
      WHERE sl.resource_id = technical_riders.id
        AND sl.resource_type = 'technical_rider'
        AND (sl.link_type = 'view' OR sl.link_type = 'edit') -- Allow viewing if edit link is used for view page
        AND (sl.expires_at IS NULL OR sl.expires_at > now())
        -- This policy doesn't check user_claimed_shares, it's for direct access via share_code
        -- The RPC get_shared_link_by_code should handle share_code validation
    )
  );

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Sep 30, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Consolidate duplicated PDF generation logic

The PDF generation logic is duplicated in AllRiders.tsx and ProductionPage.tsx.
This should be extracted into a reusable utility function to improve
maintainability.

Examples:

apps/web/src/pages/AllRiders.tsx [178-619]
  const prepareAndExecuteRiderExport = async (riderId: string, type: "color" | "print") => {
    setIsExporting(true);
    setShowExportModal(false);
    try {
      const { data, error: fetchError } = await supabase
        .from("technical_riders")
        .select("*")
        .eq("id", riderId)
        .single();


 ... (clipped 432 lines)
apps/web/src/pages/ProductionPage.tsx [937-1384]
  const prepareAndExecuteRiderExport = async (riderId: string, type: "color" | "print") => {
    setExportingItemId(riderId);
    setShowRiderExportModal(false);
    try {
      const { data, error } = await supabase
        .from("technical_riders")
        .select("*")
        .eq("id", riderId)
        .single();
      if (error || !data) throw error || new Error("Technical Rider not found");

 ... (clipped 438 lines)

Solution Walkthrough:

Before:

// In apps/web/src/pages/AllRiders.tsx
const AllRiders: React.FC = () => {
  // ...
  const prepareAndExecuteRiderExport = async (riderId, type) => {
    // ...
    if (type === "print") {
      const pdf = new jsPDF();
      // ... ~400 lines of PDF generation logic ...
      pdf.save(...);
    }
  };
  // ...
};

// In apps/web/src/pages/ProductionPage.tsx
const ProductionPage = () => {
  // ...
  const prepareAndExecuteRiderExport = async (riderId, type) => {
    // ...
    if (type === "print") {
      const pdf = new jsPDF();
      // ... ~400 lines of IDENTICAL PDF generation logic ...
      pdf.save(...);
    }
  };
  // ...
};

After:

// In a new file, e.g., apps/web/src/lib/pdfUtils.ts
export const generateRiderPdf = (riderData: RiderForExport) => {
  const pdf = new jsPDF();
  // ... ~400 lines of PDF generation logic ...
  pdf.save(...);
};

// In apps/web/src/pages/AllRiders.tsx
import { generateRiderPdf } from "../lib/pdfUtils";
const AllRiders: React.FC = () => {
  const prepareAndExecuteRiderExport = async (riderId, type) => {
    // ... fetch riderData
    if (type === "print") {
      generateRiderPdf(riderData);
    }
  };
};

// In apps/web/src/pages/ProductionPage.tsx
import { generateRiderPdf } from "../lib/pdfUtils";
const ProductionPage = () => {
  const prepareAndExecuteRiderExport = async (riderId, type) => {
    // ... fetch riderData
    if (type === "print") {
      generateRiderPdf(riderData);
    }
  };
};
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies over 400 lines of complex PDF generation logic duplicated across AllRiders.tsx and ProductionPage.tsx, which is a significant maintenance and consistency issue.

High
Possible issue
Fix race condition in PDF generation

Replace setTimeout with a useEffect hook in the export component to reliably
trigger PDF generation after the component has re-rendered, preventing a
potential race condition.

apps/web/src/pages/AllRiders.tsx [199-202]

-if (type === "color") {
-  setCurrentExportRider(riderData);
-  await new Promise((resolve) => setTimeout(resolve, 50));
-  await exportAsPdf(riderExportRef, riderData.name, "technical-rider-color", "#111827");
+/* In AllRiders.tsx */
+const executePdfExport = async (riderData: RiderForExport, type: "color" | "print") => {
+  if (type === "color") {
+    await exportAsPdf(riderExportRef, riderData.name, "technical-rider-color", "#111827");
+  }
+  // ... handle other types
+  // Reset state after export
+  setIsExporting(false);
+  setCurrentExportRider(null);
+};
+
+const prepareAndExecuteRiderExport = async (riderId: string, type: "color" | "print") => {
+  // ... (fetch riderData)
+  
+  // Set the rider data, which will trigger the useEffect in the export component
+  setCurrentExportRider(riderData); 
+};
+
+/* In RiderExport.tsx (and PrintRiderExport.tsx) */
+// Add an onRender callback prop
+interface RiderExportProps {
+  rider: RiderForExport;
+  onRender: () => void;
 }
 
+const RiderExport = forwardRef<HTMLDivElement, RiderExportProps>(({ rider, onRender }, ref) => {
+  useEffect(() => {
+    // The component has rendered with the latest rider data
+    onRender();
+  }, [rider, onRender]);
+
+  // ... component JSX
+});
+
+/* In AllRiders.tsx, where the export component is rendered */
+{currentExportRider && (
+  <RiderExport 
+    ref={riderExportRef} 
+    rider={currentExportRider} 
+    onRender={() => executePdfExport(currentExportRider, "color")}
+  />
+)}
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a potential race condition by using a fixed setTimeout and proposes a robust, standard React pattern using useEffect to eliminate the bug.

High
Prevent unauthenticated new rider creation

Refactor the useEffect hook to fetch user data sequentially before fetching
rider data. If a user is not authenticated when creating a new rider, redirect
them to the login page to prevent a race condition and improve user experience.

apps/web/src/pages/RiderEditor.tsx [53-125]

 useEffect(() => {
-  const fetchUser = async () => {
-    const { data } = await supabase.auth.getUser();
-    if (data.user) {
-      setUser(data.user);
+  const fetchData = async () => {
+    setLoading(true);
+    const { data: { user } } = await supabase.auth.getUser();
+
+    if (user) {
+      setUser(user);
+    } else if (id === "new") {
+      // Redirect to login or dashboard if a non-logged-in user tries to create a new rider.
+      navigate("/login"); // Or another appropriate route
+      setLoading(false);
+      return;
     }
-  };
-
-  const fetchRiderData = async () => {
-    setLoading(true);
 
     if (id === "new") {
       const newRider: RiderData = {
         name: "Untitled Technical Rider",
         artist_name: "",
         band_members: [],
         genre: "",
         contact_name: "",
         contact_email: "",
         contact_phone: "",
         input_list: [],
         pa_requirements: "",
         monitor_requirements: "",
         console_requirements: "",
         backline_requirements: [],
         artist_provided_gear: [],
         required_staff: [],
         special_requirements: "",
         power_requirements: "",
         lighting_notes: "",
         hospitality_notes: "",
         additional_notes: "",
       };
       setRider(newRider);
       setLoading(false);
       return;
     }
     // ...
   };
 
-  fetchUser();
-  fetchRiderData();
+  fetchData();
 }, [id, navigate]);

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies a race condition and logic flaw where an unauthenticated user could access the new rider creation form, leading to a poor user experience. The proposed fix to serialize the data fetching and add an authentication check is a robust solution that improves the component's reliability.

Medium
Avoid duplicate channel numbers on duplication
Suggestion Impact:The commit added channel_number: "" to the duplicated InputChannel object in handleDuplicateInput, matching the suggestion to prevent duplicates.

code diff:

@@ -33,6 +33,7 @@
       const duplicated: InputChannel = {
         ...inputToDuplicate,
         id: uuidv4(),
+        channel_number: "", // Clear channel number to avoid duplicates
       };
       const index = inputList.findIndex((input) => input.id === id);
       const newList = [...inputList];

In the handleDuplicateInput function, clear the channel_number of the duplicated
InputChannel to prevent duplicate channel numbers and prompt the user to assign
a new one.

apps/web/src/components/rider/RiderInputList.tsx [30-42]

 const handleDuplicateInput = (id: string) => {
   const inputToDuplicate = inputList.find((input) => input.id === id);
   if (inputToDuplicate) {
     const duplicated: InputChannel = {
       ...inputToDuplicate,
       id: uuidv4(),
+      channel_number: "", // Clear channel number to avoid duplicates
     };
     const index = inputList.findIndex((input) => input.id === id);
     const newList = [...inputList];
     newList.splice(index + 1, 0, duplicated);
     onUpdateInputList(newList);
   }
 };

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out that duplicating an input also duplicates its channel number, which should be unique. Clearing the channel number on the new item is a good user experience improvement that prevents data inconsistency and guides the user to enter a correct value.

Low
Security
Enforce share code existence in RLS

Update the Public can select technical riders via valid view share code RLS
policy to include a check for sl.share_code IS NOT NULL. This ensures that
public access is only granted when a share code explicitly exists, preventing a
security vulnerability.

supabase/migrations/20250930110000_add_technical_riders_to_shared_links.sql [126-140]

 CREATE POLICY "Public can select technical riders via valid view share code"
   ON public.technical_riders FOR SELECT
   TO public, authenticated
   USING (
     EXISTS (
       SELECT 1
       FROM public.shared_links sl
       WHERE sl.resource_id = technical_riders.id
         AND sl.resource_type = 'technical_rider'
-        AND (sl.link_type = 'view' OR sl.link_type = 'edit') -- Allow viewing if edit link is used for view page
+        AND sl.share_code IS NOT NULL -- Ensure a share code exists
+        AND (sl.link_type = 'view' OR sl.link_type = 'edit')
         AND (sl.expires_at IS NULL OR sl.expires_at > now())
-        -- This policy doesn't check user_claimed_shares, it's for direct access via share_code
-        -- The RPC get_shared_link_by_code should handle share_code validation
     )
   );
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: This suggestion identifies a critical security vulnerability in the Row Level Security policy. The policy is too permissive and could allow unauthorized access to any rider with an active share link, without needing the share code. The proposed fix correctly tightens the policy, making it a crucial security improvement.

High
General
Unify PDF generation by using components

Refactor the print-friendly PDF export to use the existing PrintRiderExport.tsx
component with html2canvas, unifying the export logic with the color version and
removing complex procedural code.

apps/web/src/pages/AllRiders.tsx [199-208]

 if (type === "color") {
   setCurrentExportRider(riderData);
   await new Promise((resolve) => setTimeout(resolve, 50));
   await exportAsPdf(riderExportRef, riderData.name, "technical-rider-color", "#111827");
-} else {
-  // Use jsPDF directly with autoTable for print-friendly version
-  try {
-    const pdf = new jsPDF("p", "pt", "letter");
-...
+} else { // "print"
+  setCurrentExportRider(riderData);
+  await new Promise((resolve) => setTimeout(resolve, 50));
+  await exportAsPdf(
+    printRiderExportRef, // Use the ref for the print component
+    riderData.name,
+    "technical-rider-print",
+    "#ffffff" // Use a white background for print
+  );
+}

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: This is an excellent suggestion that identifies inconsistent implementation and unused code, proposing a refactor that would significantly improve code quality, consistency, and maintainability.

Medium
Refactor duplicated code into a utility

Extract the duplicated PDF export functions (prepareAndExecuteRiderExport and
exportAsPdf) from AllRiders.tsx and ProductionPage.tsx into a shared utility
file to improve maintainability.

apps/web/src/pages/AllRiders.tsx [178-208]

-const prepareAndExecuteRiderExport = async (riderId: string, type: "color" | "print") => {
-  setIsExporting(true);
-  setShowExportModal(false);
-  try {
-    const { data, error: fetchError } = await supabase
-      .from("technical_riders")
-      .select("*")
-      .eq("id", riderId)
-      .single();
+/* In a new file: src/lib/riderExportUtils.ts */
+import { jsPDF } from "jspdf";
+import autoTable from "jspdf-autotable";
+import html2canvas from "html2canvas";
+import { RiderForExport } from "./types";
 
-    if (fetchError || !data) throw fetchError || new Error("Technical Rider not found");
+// ... (move exportAsPdf and prepareAndExecuteRiderExport logic here)
+// The function signature should be adapted to accept all necessary dependencies
+// (e.g., refs, state setters, supabase client) as arguments.
 
-    const riderData = {
-      ...data,
-      band_members: data.band_members || [],
-      input_list: data.input_list || [],
-      backline_requirements: data.backline_requirements || [],
-      artist_provided_gear: data.artist_provided_gear || [],
-      required_staff: data.required_staff || [],
-    } as RiderForExport;
+/* In AllRiders.tsx and ProductionPage.tsx */
+import { prepareAndExecuteRiderExport } from "../lib/riderExportUtils";
 
-    if (type === "color") {
-      setCurrentExportRider(riderData);
-      await new Promise((resolve) => setTimeout(resolve, 50));
-      await exportAsPdf(riderExportRef, riderData.name, "technical-rider-color", "#111827");
-    } else {
-      // Use jsPDF directly with autoTable for print-friendly version
-      try {
-        const pdf = new jsPDF("p", "pt", "letter");
-...
+// ... inside the component
+const handleExport = (riderId: string, type: "color" | "print") => {
+  // Call the centralized export function, passing required state setters and refs
+  prepareAndExecuteRiderExport({
+    riderId,
+    type,
+    supabase,
+    setIsExporting,
+    setShowExportModal,
+    setCurrentExportRider,
+    riderExportRef,
+    setError,
+  });
+};

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies significant code duplication for PDF export logic between AllRiders.tsx and ProductionPage.tsx and proposes a valid refactoring that improves maintainability.

Medium
  • Update

@cj-vana cj-vana merged commit 8333343 into beta Sep 30, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant